package Computer;

import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;

import Process.CalculatingProcess;
import Process.Consumer;
import Process.DiskProcess;
import Process.IdleProcess;
import Process.Process;
import Process.Process.SysCall;
import Process.Producer;
import Process.UserInput;

/**
 * Computer class.
 * 
 * @author Stephen Amos, Zach Turk, Dickson Kwong
 *
 */

public class Computer {
	
	private final static int TIMERINTERVAL = 10;
	private final static int QUANTUMINTERVAL = 200;
	
	private final static List<Process> processes = new ArrayList<Process>();
	private final static List<PCB> pcbs = new ArrayList<PCB>();
	private final static CPU cpu = new CPU(pcbs);
	private final static List<Device> devices = new ArrayList<Device>();
	private final static Device video = new Video();
	private final static Keyboard keyboard = new Keyboard();
	private final static Device disk = new Disk();
	private final static SharedMemory mem = new SharedMemory();
	private final static Scheduler scheduler = new Scheduler();
	private final static Queue<Handler> interrupt_queue = new PriorityQueue<Handler>();
	private static int pc = -1;
	private static long start_time = 0;
	private final static List<String> args = new ArrayList<String>();
		
	
	//inaccessible to all
	private Computer() { }
	
	public static void start() {
		// add devices
		devices.add(video);
		devices.add(disk);
		
		// add processes for testing purposes only.
		//addProcess("pc");
		//addProcess("k");
		//addProcess("c");
		//addProcess("d");
		getArgs();
		addProcess("i");
		
		// loop the cpu until every process is finished
		while (!cpu.isFinished()) {
			pc++;
			
			//check for timer interrupts
			if (pc % TIMERINTERVAL == 0) interrupt_queue.add(Handler.TIMER);
			//check for keyboard interrupts
			if (!keyboard.isBufferEmpty()) {
				interrupt_queue.add(Handler.KEYBOARD);
			}
			//check for 
			//check for other interrupts
			for (Device d : devices) {
				Handler h = d.poll();
				if (h != null) interrupt_queue.add(h);
			}

			
			
			//handle interrupts
			Handler interrupt = interrupt_queue.poll();
			if (interrupt != null) {
				switch(interrupt) {
				case TIMER: 
					timer();
					System.out.println("------------------------------------------------");
					System.out.println("--------State Information In this Quantum-------");
					System.out.println("------------------------------------------------");
					System.out.println("Currently Running Process: " + processes.get(cpu.running().getPID()).getClass().getSimpleName());
					System.out.println("Currently Interrupted Process: " + (cpu.interrupted() == null ? "[EMPTY]" : processes.get(cpu.interrupted().getPID()).getClass().getSimpleName()));
					System.out.println("Ready Queue: " + printReadyQueue());
					System.out.println("Interrupt Queue: " + printInterruptQueue());
					System.out.println("Mutex Collection State: " + mem.printMutexStatus());
					//System.out.println("Waiting Queue: ");
					//System.out.println("Blocking Queue: ");
					
					System.out.println("------------------------------------------------");
					System.out.println("--------Event log of this Quantum---------------");
					System.out.println("------------------------------------------------");
					;break;
				case KEYBOARD: keyboard(); break;
				case DISK: disk(); break;
				case VIDEO: video(); break;
				case SEMDOWN: semdown(); break;
				case SEMUP: semup(); break;
				case MEMREAD: memread(); break;
				case MEMWRITE: memwrite(); break;
				default: break;
				}
				//if there's an interrupt, don't execute any instructions this step
				//continue on to the next step
				continue;
			}
			
			//execute program (all instructions are treated as atomic)
			PCB runningPCB = cpu.running();
			if (runningPCB == null) {
				if (cpu.interrupted() != null) {
					cpu.setRunning(cpu.interrupted());
					runningPCB = cpu.running();
				} 
			}
			Process running = processes.get(runningPCB.getPID());
			SysCall sc = running.next();
			if (sc != null) {
				//all of these need method calls like interrupts (lines 50 - 59)
				switch(sc) {
				case IDLE: break;
				case KEYBOARD_IO: keyboard_io(); break;
				case VIDEO_IO: video_io(); break;
				case DISK_IO: disk_io(); break;
				case MEM_READ: mem_read(); break;
				case MEM_WRITE: mem_write(); break;
				case SEM_DOWN: sem_down(); break;
				case SEM_UP: sem_up(); break;
				case MUTEX_LOCK: lock(); break;
				case MUTEX_UNLOCK: unlock(); break;
				default: break;
				}
			}
		}
		
	}
	
	/**
	 * Timer interrupt handler.
	 * Run next ready process.
	 */
	private static void timer() {
		try {
      		Thread.sleep(1000);
  		} catch (InterruptedException e) {}
		
		System.out.println("\n\nTimer Interrupt: " + pc / TIMERINTERVAL);
		
		if (cpu.running() == null) System.out.println("No process currently running");
		else System.out.println(processes.get(cpu.running().getPID()).getClass().getSimpleName() + " preempted, put in ready queue");
		cpu.addPCB(cpu.running());
		cpu.setRunning(pcb(scheduler.next(cpu.ready())));
		System.out.println(processes.get(cpu.running().getPID()).getClass().getSimpleName() + " running");
	}
	
	/**
	 * IO interrupt handler.
	 * Print something to the screen.
	 */
	private static void video() {
		System.out.println("Interrupt: Video Ready");

		interruptHandler(video);
	}
	
	/**
	 * Disk interrupt handler.
	 */
	private static void disk() {
		System.out.println("Interrupt: Disk Ready");
		interruptHandler(disk);
	}
	
	private static void keyboard() {
		System.out.println("Interrupt: Keyboard ready");
		System.out.println("\tCurrently running process interrupted");
		cpu.setInterrupted(cpu.running());
		if (!keyboard.isBufferEmpty()) {
			System.out.println("\t\tKey input detected. Key typed: " + keyboard.readFromBuffer());
			PCB blocked = keyboard.removeBlocked();
			if (blocked != null) {
				System.out.println("\t\tBlocked process " + processes.get(blocked.getPID()).getClass().getSimpleName() + " moved to ready");
				cpu.ready().add(blocked);
			} else System.out.println("\t\tNothing waiting on device");
		}
		System.out.println("\tProcess resumed");
		cpu.setRunning(cpu.interrupted());
	}
	
	private static void semup() {
		System.out.println("\tTrap: Semaphore currently down, setting to up");
		//System.out.println(processes.get(cpu.running().getPID()).getClass().getSimpleName() + ": Sem Down Trap Handler, ready for new owner");
		Process running = processes.get(cpu.running().getPID());
		int addr = 0;
		if (running instanceof Producer) {
			addr = ((Producer) running).memAddress();
		} else if (running instanceof Consumer) {
			addr = ((Consumer) running).memAddress();
		}
		mem.getList(addr).poll();
		if (mem.getList(addr).size() > 0) {
			cpu.ready().add(mem.getList(addr).peek());
			System.out.println("\t\t" + processes.get(mem.getList(addr).peek().getPID()).getClass().getSimpleName() + " is now owner");
		}
	}
	
	
	private static boolean semdown() {
		//System.out.println(processes.get(cpu.running().getPID()).getClass().getSimpleName() + ": Sem Up Trap Handler, requesting ownership");
		
		boolean result = false;
		
		Process running = processes.get(cpu.running().getPID());
		int addr = 0;
		if (running instanceof Producer) {
			addr = ((Producer) running).memAddress();
		} else if (running instanceof Consumer) {
			addr = ((Consumer) running).memAddress();
		}
		
		Queue<PCB> sem = mem.getList(addr);
		
		// add the pcb to the semaphore
		sem.add(cpu.running());
		// if pcb becomes owner
		if (sem.size() == 1) {
			result = true; 
			System.out.println("\tTrap: Semaphore currently up, setting to down");
		}
		// if pcb not owner
		else {
			System.out.println("\tTrap: Semaphore already down");
			System.out.println("\t\t" + processes.get(cpu.running().getPID()).getClass().getSimpleName() +" blocked until SemUp");
			cpu.ready().remove(cpu.ready());
			cpu.setRunning(pcb(scheduler.next(cpu.ready())));
			System.out.println(processes.get(cpu.running().getPID()).getClass().getSimpleName() + " running");
		}
		
		// remove from cpu and place into sem queue
		
		return result;
	}
	
	private static void memread() {
		Consumer c = (Consumer) processes.get(cpu.running().getPID());
		int addr = c.memAddress();
		System.out.println("Memory read via consumer: " + mem.getValue(addr));
	}
	
	
	private static void memwrite() {
		Producer p = (Producer) processes.get(cpu.running().getPID());
		p.addToData();
		System.out.println("Memory write via producer: " + p.getList());
		mem.write(p.memAddress(), p.getList());
	}
	
	private static void interruptHandler(Device d) {
		System.out.println("\tProcess interrupted");
	  	cpu.setInterrupted(cpu.running());
		PCB p = d.getProcess();
		if (p != null) {
			System.out.println("\t\tBlocked process " + processes.get(p.getPID()).getClass().getSimpleName() + " moved to ready");
	  		cpu.addPCB(p);
	  		
//	  		cpu.setRunning(cpu.interrupted());
//	  		System.out.println("Running: " + processes.get(cpu.running().getPID()).getClass().getSimpleName());
		} else System.out.println("\t\tNothing waiting on device");
		System.out.println("\tProcess resumed");
		cpu.setRunning(cpu.interrupted());
		
	}
	
	/*private static void mutexLock() {
		System.out.println(processes.get(cpu.running().getPID()).getClass().getSimpleName() +" has gained ownership of the Semaphore - locking mutex");
	}
	
	private static void mutexUnlock() {
		System.out.println(processes.get(cpu.running().getPID()).getClass().getSimpleName() +" has released ownership of the Semaphore - unlocking mutex");
	}*/
	
	private static void keyboard_io() {
		System.out.println("SysCall: " + processes.get(cpu.running().getPID()).getClass().getSimpleName() + " requesting char from keyboard, blocked until keyboard next ready");
		keyboard.addBlocked(cpu.running());
		cpu.setRunning(cpu.ready().poll());
	}
	
	private static void video_io() {
		
	}
	
	private static void disk_io() {
		System.out.println("SysCall: " + processes.get(cpu.running().getPID()).getClass().getSimpleName() + " requesting disk, blocked until disk next available");
		disk.addProcess(cpu.running());
		cpu.setRunning(cpu.ready().poll());
		System.out.println("Running: " + processes.get(cpu.running().getPID()).getClass().getSimpleName());
	}
	
	private static void sem_down() {
		System.out.println("SysCall: " + processes.get(cpu.running().getPID()).getClass().getSimpleName() + " requesting semaphore ownership (SemDown)");
		semdown();
	}
	
	private static void sem_up() {
		System.out.println("SysCall: " + processes.get(cpu.running().getPID()).getClass().getSimpleName() + " relinquishing semaphore ownership (SemUp)");
		semup();
		System.out.println(processes.get(cpu.running().getPID()).getClass().getSimpleName() + " voluntarily preempted");
		cpu.addPCB(cpu.running());
		
		cpu.setRunning(pcb(scheduler.next(cpu.ready())));
		System.out.println(processes.get(cpu.running().getPID()).getClass().getSimpleName() + " running");
	}
	
	private static void lock() {
		System.out.println("SysCall: " + processes.get(cpu.running().getPID()).getClass().getSimpleName() + " locked the mutex");
	}
	
	private static void unlock() {
		System.out.println("SysCall: " + processes.get(cpu.running().getPID()).getClass().getSimpleName() + " unlocked the mutex");
	}
	
	private static void mem_write() {
		memwrite();
	}
	
	private static void mem_read() {
		memread();
	}
	
	public static void addProcess(String process) {
		System.out.println("Creating Process: " + process);
		int id = processes.size();
		SysCall[] calls = new SysCall[10];
		Process p = null;
		PCB pcb = new PCB(id, 0);
		
		if (process.equals("c")) {
			p = new CalculatingProcess(id, calls);
			
		} else if (process.equals("pc")) {
			int producer_count = 0;
			for (Process the_process : processes) {
				if (the_process instanceof Producer) producer_count++;
			}
			p = new Producer(id+1, calls, producer_count);
			pcb = new PCB(id+1, 0);
			
			//create a new calls array so we can don't overwrite our requests later
			calls = new SysCall[10];
			Process c = new Consumer(id, calls, producer_count);
			PCB c_pcb = new PCB(id, 0);
			processes.add(c);
			pcbs.add(c_pcb);
			cpu.addPCB(c_pcb);
			
		} else if (process.equals("k")) {
			p = new UserInput(id, calls, 0);
			p.addObserver(keyboard);
			
		} else if (process.equals("d")) {
			p = new DiskProcess(id, calls, 0);
		} else if (process.equals("i")) {
			p = new IdleProcess(id, calls);
		}
		processes.add(p);
		pcbs.add(pcb);
		cpu.addPCB(pcb);
		if (cpu.running() == null) {
			cpu.setRunning(pcb(scheduler.next(cpu.ready())));
		}
	}
	
	public static void getArgs() {
		for (String a : args) {
			addProcess(a);
		}
	}
	
	public static void setArgs(String[] the_args) {
		for (String a : the_args) {
			//System.err.println("Added: " + a);
			args.add(a);
		}
	}
		
	/**
	 * @param id
	 * @return Returns a PCB by PID
	 */
	public static PCB pcb(int id) {
		for (PCB p : pcbs) if (p.getPID() == id) return p;
		return null;
	}
	
	public static String printReadyQueue(){
		StringBuilder the_string = new StringBuilder();
		for (PCB the_queue: cpu.ready()) {
			the_string.append("[");
			the_string.append(processes.get(the_queue.getPID()).getClass().getSimpleName());
			the_string.append("], ");
		}
		return the_string.toString();
	}
	
	public static String printInterruptQueue(){
		StringBuilder the_string = new StringBuilder();
		
		if (interrupt_queue.size() == 0){
			return "[EMPTY]";
		}
		
		for (Handler the_handler : interrupt_queue) {
			the_string.append("[");
			the_string.append(the_handler.name());
			the_string.append("], ");
		}		
		return the_string.toString();
	}
	
	public enum Handler {
		TIMER, KEYBOARD, DISK, VIDEO, SEMDOWN, SEMUP, MEMREAD, MEMWRITE;
	}
}